<html>
  <head>
    <meta charset="utf-8" />
<meta name="description" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>用 MotionLayout 来做过渡动画 | Jiang</title>
<link rel="shortcut icon" href="https://cherrylover.github.io/favicon.ico?v=1601447716515">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
<link rel="stylesheet" href="https://cherrylover.github.io/styles/main.css">

<script src="https://cdn.bootcss.com/highlight.js/9.12.0/highlight.min.js"></script>
<script src="https://cdn.bootcss.com/moment.js/2.23.0/moment.min.js"></script>


<script async src="https://www.googletagmanager.com/gtag/js?id=UA-143284233-1"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'UA-143284233-1');
</script>

  </head>
  <body>
    <div class="main">
      <div class="main-content">
        <div class="site-header">
  <a href="https://cherrylover.github.io">
  <img class="avatar" src="https://cherrylover.github.io/images/avatar.png?v=1601447716515" alt="">
  </a>
  <h1 class="site-title">
    Jiang
  </h1>
  <p class="site-description">
    我允许你走进我的世界，但不许你在我的世界里走来走去。
  </p>
  <div class="menu-container">
    
      
        <a href="/" class="menu">
          首页
        </a>
      
    
      
        <a href="/archives" class="menu">
          归档
        </a>
      
    
      
        <a href="/tags" class="menu">
          标签
        </a>
      
    
      
        <a href="https://cherrylover.github.io/post/guan-yu-wo/" class="menu">
          关于
        </a>
      
    
  </div>
  <div class="social-container">
    
      
        <a href="https://github.com/CherryLover" target="_blank">
          <i class="fab fa-github"></i>
        </a>
      
    
      
    
      
        <a href="https://juejin.im/user/576d73cd0a2b58006a09ad87" target="_blank">
          <i class="fab fa-weibo"></i>
        </a>
      
    
      
    
      
    
  </div>
</div>

      
        <div class="post-detail">
          <article class="post">
            <h2 class="post-title">
              用 MotionLayout 来做过渡动画
            </h2>
            <div class="post-info">
              <time class="post-time">
                · 2019-07-13 ·
              </time>
              
                <a href="https://cherrylover.github.io/tag/J97-m2V0f/" class="post-tags">
                  # Jetpack 追新
                </a>
              
                <a href="https://cherrylover.github.io/tag/37yorOBW4/" class="post-tags">
                  # 持续学习
                </a>
              
                <a href="https://cherrylover.github.io/tag/Mk4HmBIQ7/" class="post-tags">
                  # Android
                </a>
              
            </div>
            
              <div class="post-feature-image" style="background-image: url('https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/blog/motionLayout/nasa-V4ZksNimxLk-unsplash.jpg')">
              </div>
            
            <div class="post-content">
              <p>MotionLayout 是一个 Google 官方出品用于制作 Android 中的过渡动画的框架。用来它就能轻松的做出一些较为复杂的动画效果。</p>
<blockquote>
<p>由于 MotionLayout 是基于 ConstraintLayout ，所以其中涉及到了部分关于 ConstraintLayout 的基本知识，本文按下不表，对 ConstraintLayout 不熟悉的同学，可以查看鸿洋的<a href="https://blog.csdn.net/lmj623565791/article/details/78011599">这篇博客</a>。</p>
</blockquote>
<p>MotionLayout 是 ConstraintLayout 的子类，并且在 ConstraintLayout 发展到 2.0 时才加入 ConstraintLayout 这个库，本文所使用的依赖为：</p>
<pre><code class="language-groovy">implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2'
</code></pre>
<p>接下来让我们进入正题，先来看看我用 MotionLayout 制作的一个 Demo。</p>
<figure data-type="image" tabindex="1"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/blog/motionLayout/blog_motion_login.gif" alt="" loading="lazy"></figure>
<p>在这个例子中，当点击 Login 按钮时，Login 按钮的长度进行不断缩小，缩小到一定尺寸时，外层的 ProgressBar 还是逐渐由不可见变为可见，同时，Login 按钮上的字进行了淡入淡出的动画效果。</p>
<p>MotionLayout 能做的不仅如此，它还能做到其他更为好玩有趣的过渡动画。现在让我们来学一下吧。</p>
<p>过渡动画，顾名思义就是在状态之间进行过渡的动画效果，防止页面内 View 出现瞬间移动的效果。而 MotionLayout 的重点其实就是状态。开发者只需要定义好对应状态下 View 的相对位置，以及相关属性，其后 MotionLayout 便会自动为其增加动的效果。</p>
<figure data-type="image" tabindex="2"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/blog/motionLayout/blog_motion_login_1.gif" alt="" loading="lazy"></figure>
<p>这样的一个最简单的效果是怎么做出来的呢？</p>
<p>首先我们需要在资源文件夹 <code>res</code> 下新建一个名为 xml 的资源文件夹，然后再 xml 文件夹内新建一个根节点是 <code>MotionScene</code> 的 xml 文件，demo 中这个 xml 的文件名为 login_animator。</p>
<p>以下就是实现 Login 按钮长度变换的过渡动画。</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;MotionScene xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;&gt;

    &lt;Transition
        app:constraintSetEnd=&quot;@id/a_login_end&quot;
        app:constraintSetStart=&quot;@id/a_login_start&quot;
        app:duration=&quot;1000&quot;&gt;
        &lt;OnClick
            app:clickAction=&quot;toggle&quot;
            app:targetId=&quot;@id/tv_action_login&quot; /&gt;

    &lt;/Transition&gt;

    &lt;ConstraintSet android:id=&quot;@+id/a_login_start&quot;&gt;
        &lt;Constraint android:id=&quot;@+id/tv_action_login&quot;&gt;
            &lt;Layout
                android:layout_width=&quot;match_parent&quot;
                android:layout_height=&quot;48dp&quot;
								android:layout_marginTop=&quot;30dp&quot;
                android:layout_marginStart=&quot;30dp&quot;
                android:layout_marginEnd=&quot;30dp&quot;
                app:layout_constraintTop_toBottomOf=&quot;@id/et_passwd&quot; /&gt;
        &lt;/Constraint&gt;

    &lt;/ConstraintSet&gt;

    &lt;ConstraintSet android:id=&quot;@+id/a_login_end&quot;&gt;
        &lt;Constraint android:id=&quot;@+id/tv_action_login&quot;&gt;
            &lt;Layout
                android:layout_width=&quot;48dp&quot;
                android:layout_height=&quot;48dp&quot;
                android:layout_marginTop=&quot;30dp&quot;
                app:layout_constraintEnd_toEndOf=&quot;@+id/et_account&quot;
                app:layout_constraintStart_toStartOf=&quot;@+id/et_account&quot;
                app:layout_constraintTop_toBottomOf=&quot;@id/et_passwd&quot; /&gt;
        &lt;/Constraint&gt;

    &lt;/ConstraintSet&gt;

&lt;/MotionScene&gt;
</code></pre>
<p>仔细看其中的信息，其中大部分是我们都熟悉的，无非就是对 View 的相对位置的约定或是 View 自身属性的规定，少部分是关于过渡动画的。</p>
<p>我们先来看看这个文件的整体结构，首先根节点是 MotionScene ，MotionScene 节点下有一个 Transition 与两个 ConstraintSet 节点，而且 Transition 中有两个属性，一个是 constraintSetStart 另一个是 constraintSetEnd，这两个属性的值正好是两个 ConstraintSet 节点的 id，而 Transition 内子节点 OnClick 节点内的属性 targetId 则表明了当前 Transition 所指定的动画是作用于具体的 View 上。</p>
<p>如你所想，通过在 Transition 内指定某个 View 的两个状态下的不同属性，就能产生在这两个状态内的过渡动画，并且在 Translation 内通过组合不同的动画事件进行显示。比如点击产生的动画（OnClick），滑动产生的动画（OnSwipe），以及可改变某一帧动画效果的关键帧动画（KeyFrameSet）。</p>
<p>当我们把初始及结束状态下的属性及动画定义完成后，还需要回到我们的布局文件，将需要实现过渡动画的 View 的父布局改为 MotionLayout 并且给它添加一个值为刚才我们新建那个 xml 文件的引用的属性 layoutDescription。</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;androidx.constraintlayout.motion.widget.MotionLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    app:layoutDescription=&quot;@xml/login_animator&quot;&gt;
    ......
&lt;/androidx.constraintlayout.motion.widget.MotionLayout&gt;
</code></pre>
<p>这就是一个最为简单的使用 MotionLayout 实现过渡动画的例子，它与开头我自己写的那个 demo 没什么差别，无非就是 demo 中变换的 View 的个数及属性多少不同而已。</p>
<p>在这个例子中，我们通过在 Transition 中定义了一个 OnClick 的子节点，而达到点击产生动画的效果。其中，targetId 即为产生动画效果的目标 View 的 id；clickAction 则是指明在是在开始或是再结束状态时产生动画，toggle 表示在开始和结束状态时均有效，它还有 transitionToStart 和 transitionToEnd 表示只在开始或是结束状态下有效。有兴趣的可以去试试。</p>
<p>除了 OnClick，我们还可以在 Translation 中定义 OnSwipe 节点，OnSwipe 就是用来处理屏幕上的滑动事件，以此配合指定的 View 实现过渡动画的效果。</p>
<figure data-type="image" tabindex="3"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/blog/motionLayout/blog_motion_swipe_short.gif" alt="" loading="lazy"></figure>
<blockquote>
<p>给 MotionLayout 添加 motionDebug=&quot;SHOW_PATH&quot; 这个属性，即可查看 View 的过渡动画的轨迹。</p>
</blockquote>
<p>通过指定 View 的开始状态（靠近屏幕左边）和结束状态（靠近屏幕右边），然后在 Translation 中声明出滑动事件，即可。</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;MotionScene xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;&gt;

    &lt;Transition
        app:constraintSetEnd=&quot;@id/v_swipe_end&quot;
        app:constraintSetStart=&quot;@id/v_swipe_start&quot;
        app:duration=&quot;1000&quot;&gt;
        &lt;OnSwipe
            app:dragDirection=&quot;dragRight&quot;
            app:touchRegionId=&quot;@id/v_swipe&quot; /&gt;

    &lt;/Transition&gt;
    
    &lt;ConstraintSet android:id=&quot;@+id/v_swipe_start&quot;&gt;
        &lt;Constraint android:id=&quot;@+id/v_swipe&quot;&gt;
            &lt;Layout
                android:layout_width=&quot;48dp&quot;
                android:layout_height=&quot;48dp&quot;
                app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
                app:layout_constraintStart_toStartOf=&quot;parent&quot;
                app:layout_constraintTop_toTopOf=&quot;parent&quot; /&gt;
        &lt;/Constraint&gt;

    &lt;/ConstraintSet&gt;

    &lt;ConstraintSet android:id=&quot;@+id/v_swipe_end&quot;&gt;
        &lt;Constraint android:id=&quot;@+id/v_swipe&quot;&gt;
            &lt;Layout
                android:layout_width=&quot;48dp&quot;
                android:layout_height=&quot;48dp&quot;
                app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
                app:layout_constraintEnd_toEndOf=&quot;parent&quot;
                app:layout_constraintTop_toTopOf=&quot;parent&quot; /&gt;
        &lt;/Constraint&gt;

    &lt;/ConstraintSet&gt;

&lt;/MotionScene&gt;
</code></pre>
<p>在 OnSwipe 中，有两个属性，一个是 dragDirection 代表的是滑动的方向，touchRegionId 则指明了监听的滑动区域为 View 的滑动区域。既然能作用于 View 的滑动区域，是不是也能作用于整个屏幕的滑动区域呢？没错，touchAnchorId 则表示全部的滑动区域。OnSwipe 还有一些其他属性，比如：touchAnchorSide 表示监听 View 的哪个区域的滑动监听，如果不设置的话，是 View 外的所有区域；onTouchUp 表示当在滑动过程中手指抬起时动画的动作（回到开始状态、回到结束状态、自动完成、停止等等）。</p>
<p>说实话，我在开始尝试 MotionLayout 的时候被 OnSwipe 给吓到了，但是当我更进一步的使用 KeyFrameSet 的时候直喊 666。原因就是因为 KeyFrameSet 能做出更炫酷的效果。</p>
<p>KeyFrameSet 是作用于在过渡动画过程中的关键帧，通过指定动画关键进程时的状态来实现不同的效果。举个例子，当前的 View 滑动是一条直线，我想让在滑动过程中有一个先向上滑动，然后向下滑动以这种效果达到屏幕的最右侧。</p>
<figure data-type="image" tabindex="4"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/blog/motionLayout/blog_motion_swipe_frame.gif" alt="" loading="lazy"></figure>
<p>View 的开始与结束状态没有发生改变，只是在过渡动画的中点区域进行改变 View 的坐标。</p>
<pre><code class="language-xml">&lt;Transition
    app:constraintSetEnd=&quot;@id/v_swipe_end&quot;
    app:constraintSetStart=&quot;@id/v_swipe_start&quot;
    app:duration=&quot;1000&quot;&gt;
    &lt;OnSwipe
        app:dragDirection=&quot;dragRight&quot;
        app:touchAnchorId=&quot;@id/v_swipe&quot;
        app:touchAnchorSide=&quot;bottom&quot; /&gt;

    &lt;KeyFrameSet&gt;
        &lt;KeyPosition
            app:framePosition=&quot;50&quot;
            app:keyPositionType=&quot;parentRelative&quot;
            app:motionTarget=&quot;@+id/v_swipe&quot;
            app:percentY=&quot;0.3&quot; /&gt;
    &lt;/KeyFrameSet&gt;

&lt;/Transition&gt;
</code></pre>
<p>framePosition 表示在运动到整个运动过程的 50% 处，这个值的取值范围是 0 - 100，motionTarget 表示作用的 View，而 keyPositionType 与percentY 则共同决定了运动轨迹中弧度的变化方向。keyPositionType 控制 percentY 的坐标系的工作方式，它一共有 3 个值。parentRelative、deltaRelative、pathRelative。percentY 取值范围为 0 - 1，同时允许负数及大于 1 的值。</p>
<p>parentRelative 表示，坐标按照父布局的坐标进行处理，X，Y 轴的最大值均为1，X 轴向右为正，向左为负，Y 轴向下为正，向上为负。</p>
<figure data-type="image" tabindex="5"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/blog/motionLayout/parentRelative'.png" alt="" loading="lazy"></figure>
<p>deltaRelative 表示开始状态的中心点为坐标系原点，X，Y 轴的最大值均为1，X 轴向右为正，向左为负，Y 轴向下为负，向上为正。</p>
<figure data-type="image" tabindex="6"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/blog/motionLayout/deltaRelative.png" alt="" loading="lazy"></figure>
<p>pathRelative 表示开始状态的中心点为坐标系原点，X 轴为两个状中心点的构成的直线。X，Y 轴的最大值均为1，X 轴向结束状态方向为正，向开始状态方向为负，Y 轴向下为负，向上为正。</p>
<figure data-type="image" tabindex="7"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/blog/motionLayout/pathRelative.png" alt="" loading="lazy"></figure>
<blockquote>
<p>keyPositionType 三个属性的描述图均来自 <a href="https://codelabs.developers.google.com/codelabs/motion-layout/index.html?index=..%2F..index#4">CodeLab</a></p>
</blockquote>
<p>KeyPostition 还有些其他有趣的属性，比如，控制运动轨迹是平滑的曲线还是直线的 curveFit，以及 transitionEasing 控制运动过程的加速或是减速等等。这里就不一一举例了。</p>
<p>而且，还可以同时存在多个关键帧进行控制动画效果。</p>
<pre><code class="language-xml">&lt;KeyFrameSet&gt;
    &lt;KeyPosition
        app:framePosition=&quot;50&quot;
        app:keyPositionType=&quot;parentRelative&quot;
        app:motionTarget=&quot;@+id/v_swipe&quot;
        app:percentY=&quot;0.3&quot; /&gt;
    &lt;KeyAttribute
        android:alpha=&quot;0&quot;
        app:framePosition=&quot;50&quot;
        app:motionTarget=&quot;@+id/v_swipe&quot; /&gt;
&lt;/KeyFrameSet&gt;
</code></pre>
<p>keyAttribute 是用于在过渡动画中控制 View 的属性，比如在动画执行 50% 时，View 的 alpha 值为 0 ，那么在从 0 - 50% 及 50% - 100% 的过程中，则由 MotionLayout 根据其执行时间自动改变 View 的状态。</p>
<figure data-type="image" tabindex="8"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/blog/motionLayout/blog_motion_swipe_attr.gif" alt="" loading="lazy"></figure>
<p>刚才聊的都是关于动画本身的内容，实际上，MotionLayout 提供更多方式来对 View 进行状态改变，不只是通过在 ConstraintSet 中指定 Layout 来改变 View 的相对位置，它还提供了更为丰富的方法进行改变 View 的状态，比如：</p>
<blockquote>
<p>Motion 用于改变动画效果，例如加速、减速、先水平方法还是先垂直方向进行移动</p>
<p>CustomAttribute 用于改变自定义属性；</p>
<p>PropertySet 用于改变 View 特定的几个属性；</p>
<p>Transform 用于改变 View 中涉及到属性动画的属性，例如：rotation、scaleX 等。用法也很简单，像 Layout 那样声明出来即可。</p>
</blockquote>
<pre><code class="language-xml">&lt;ConstraintSet android:id=&quot;@+id/v_swipe_start&quot;&gt;
    &lt;Constraint android:id=&quot;@+id/v_swipe&quot;&gt;
        &lt;CustomAttribute
            app:attributeName=&quot;backgroundColor&quot;
            app:customColorValue=&quot;@color/colorAccent&quot; /&gt;
        &lt;Transform
            android:scaleX=&quot;1.0&quot;
            android:scaleY=&quot;1.0&quot; /&gt;
        &lt;Layout
            android:layout_width=&quot;48dp&quot;
            android:layout_height=&quot;48dp&quot;
            app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
            app:layout_constraintStart_toStartOf=&quot;parent&quot;
            app:layout_constraintTop_toTopOf=&quot;parent&quot; /&gt;
    &lt;/Constraint&gt;

&lt;/ConstraintSet&gt;

&lt;ConstraintSet android:id=&quot;@+id/v_swipe_end&quot;&gt;
    &lt;Constraint android:id=&quot;@+id/v_swipe&quot;&gt;
        &lt;CustomAttribute
            app:attributeName=&quot;backgroundColor&quot;
            app:customColorValue=&quot;@color/colorPrimary&quot; /&gt;
        &lt;Transform
            android:scaleX=&quot;3.0&quot;
            android:scaleY=&quot;3.0&quot; /&gt;
        &lt;Layout
            android:layout_width=&quot;48dp&quot;
            android:layout_height=&quot;48dp&quot;
            app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
            app:layout_constraintEnd_toEndOf=&quot;parent&quot;
            app:layout_constraintTop_toTopOf=&quot;parent&quot; /&gt;
    &lt;/Constraint&gt;

&lt;/ConstraintSet&gt;
</code></pre>
<figure data-type="image" tabindex="9"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/blog/motionLayout/blog_motion_swipe_attr_more.gif" alt="" loading="lazy"></figure>
<blockquote>
<p>由于放大倍数较大，超出屏幕，所以在结束状态时显示存在异常。</p>
</blockquote>
<p>怎么样，MotionLayout 是不是比想象中的好玩一些，就是现在不太方便调试，每次调试都需要运行，不过呢，现在这个还没发布正式版，估计在正式版中 Google 应该会解决这个问题。</p>
<p>本文首发于<a href="https://jiangjiwei.site/">个人博客</a>，文中全部源代码已上传至 <a href="https://github.com/CherryLover/BlogTest">GitHub</a>，代码分支为：motionLayout。喜欢的麻烦点个🌟。</p>
<p>推荐学习网站：<a href="https://codelabs.developers.google.com/codelabs/motion-layout/index.html?index=..%2F..index#0">CodeLab</a>。</p>
<p>本文封面图：Photo by <a href="https://unsplash.com/@nasa?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">NASA</a> on <a href="https://unsplash.com/search/photos/nasa?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p>

            </div>
          </article>
        </div>
    
        
          <div class="next-post">
            <div class="next">下一篇</div>
            <a href="https://cherrylover.github.io/post/lai-xue-yi-bo-navigation/">
              <h3 class="post-title">
                来学一波 Navigation
              </h3>
            </a>
          </div>  
        

        
          
            <link rel="stylesheet" href="https://unpkg.com/gitalk/dist/gitalk.css">
<script src="https://unpkg.com/gitalk/dist/gitalk.min.js"></script>

<div id="gitalk-container"></div>

<script>

  var gitalk = new Gitalk({
    clientID: 'f5cc70d7d50fba65cf69',
    clientSecret: '1d018e38ce9cdd399682bdd76690decfef00a051',
    repo: 'CherryLover.github.io',
    owner: 'CherryLover',
    admin: ['CherryLover'],
    id: location.pathname,      // Ensure uniqueness and length less than 50
    distractionFreeMode: false  // Facebook-like distraction free mode
  })

  gitalk.render('gitalk-container')

</script>

          

          
        
    
        <div class="site-footer">
  Powered by <a href="https://github.com/getgridea/gridea" target="_blank">Gridea</a>
</div>

<script>
  hljs.initHighlightingOnLoad()
</script>

      </div>
    </div>
  </body>
</html>
